// port https://github.com/modelcontextprotocol/servers/blob/main/src/memory/index.ts
import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { promises as fs } from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

// Define memory file path using environment variable with fallback
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')

// We are storing our memory using entities, relations, and observations in a graph structure
interface Entity {
  name: string
  entityType: string
  observations: string[]
}

interface Relation {
  from: string
  to: string
  relationType: string
}

interface KnowledgeGraph {
  entities: Entity[]
  relations: Relation[]
}

// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
class KnowledgeGraphManager {
  private memoryPath: string

  constructor(memoryPath: string) {
    this.memoryPath = memoryPath
    this.ensureMemoryPathExists()
  }

  private async ensureMemoryPathExists(): Promise<void> {
    try {
      // Ensure the directory exists
      const directory = path.dirname(this.memoryPath)
      await fs.mkdir(directory, { recursive: true })

      // Check if the file exists, if not create an empty one
      try {
        await fs.access(this.memoryPath)
      } catch (error) {
        // File doesn't exist, create an empty file
        await fs.writeFile(this.memoryPath, '')
      }
    } catch (error) {
      console.error('Failed to create memory path:', error)
    }
  }

  private async loadGraph(): Promise<KnowledgeGraph> {
    try {
      const data = await fs.readFile(this.memoryPath, 'utf-8')
      const lines = data.split('\n').filter((line) => line.trim() !== '')
      return lines.reduce(
        (graph: KnowledgeGraph, line) => {
          const item = JSON.parse(line)
          if (item.type === 'entity') graph.entities.push(item as Entity)
          if (item.type === 'relation') graph.relations.push(item as Relation)
          return graph
        },
        { entities: [], relations: [] }
      )
    } catch (error) {
      if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
        return { entities: [], relations: [] }
      }
      throw error
    }
  }

  private async saveGraph(graph: KnowledgeGraph): Promise<void> {
    const lines = [
      ...graph.entities.map((e) => JSON.stringify({ type: 'entity', ...e })),
      ...graph.relations.map((r) => JSON.stringify({ type: 'relation', ...r }))
    ]
    await fs.writeFile(this.memoryPath, lines.join('\n'))
  }

  async createEntities(entities: Entity[]): Promise<Entity[]> {
    const graph = await this.loadGraph()
    const newEntities = entities.filter((e) => !graph.entities.some((existingEntity) => existingEntity.name === e.name))
    graph.entities.push(...newEntities)
    await this.saveGraph(graph)
    return newEntities
  }

  async createRelations(relations: Relation[]): Promise<Relation[]> {
    const graph = await this.loadGraph()
    const newRelations = relations.filter(
      (r) =>
        !graph.relations.some(
          (existingRelation) =>
            existingRelation.from === r.from &&
            existingRelation.to === r.to &&
            existingRelation.relationType === r.relationType
        )
    )
    graph.relations.push(...newRelations)
    await this.saveGraph(graph)
    return newRelations
  }

  async addObservations(
    observations: { entityName: string; contents: string[] }[]
  ): Promise<{ entityName: string; addedObservations: string[] }[]> {
    const graph = await this.loadGraph()
    const results = observations.map((o) => {
      const entity = graph.entities.find((e) => e.name === o.entityName)
      if (!entity) {
        throw new Error(`Entity with name ${o.entityName} not found`)
      }
      const newObservations = o.contents.filter((content) => !entity.observations.includes(content))
      entity.observations.push(...newObservations)
      return { entityName: o.entityName, addedObservations: newObservations }
    })
    await this.saveGraph(graph)
    return results
  }

  async deleteEntities(entityNames: string[]): Promise<void> {
    const graph = await this.loadGraph()
    graph.entities = graph.entities.filter((e) => !entityNames.includes(e.name))
    graph.relations = graph.relations.filter((r) => !entityNames.includes(r.from) && !entityNames.includes(r.to))
    await this.saveGraph(graph)
  }

  async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
    const graph = await this.loadGraph()
    deletions.forEach((d) => {
      const entity = graph.entities.find((e) => e.name === d.entityName)
      if (entity) {
        entity.observations = entity.observations.filter((o) => !d.observations.includes(o))
      }
    })
    await this.saveGraph(graph)
  }

  async deleteRelations(relations: Relation[]): Promise<void> {
    const graph = await this.loadGraph()
    graph.relations = graph.relations.filter(
      (r) =>
        !relations.some(
          (delRelation) =>
            r.from === delRelation.from && r.to === delRelation.to && r.relationType === delRelation.relationType
        )
    )
    await this.saveGraph(graph)
  }

  async readGraph(): Promise<KnowledgeGraph> {
    return this.loadGraph()
  }

  // Very basic search function
  async searchNodes(query: string): Promise<KnowledgeGraph> {
    const graph = await this.loadGraph()

    // Filter entities
    const filteredEntities = graph.entities.filter(
      (e) =>
        e.name.toLowerCase().includes(query.toLowerCase()) ||
        e.entityType.toLowerCase().includes(query.toLowerCase()) ||
        e.observations.some((o) => o.toLowerCase().includes(query.toLowerCase()))
    )

    // Create a Set of filtered entity names for quick lookup
    const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))

    // Filter relations to only include those between filtered entities
    const filteredRelations = graph.relations.filter(
      (r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
    )

    const filteredGraph: KnowledgeGraph = {
      entities: filteredEntities,
      relations: filteredRelations
    }

    return filteredGraph
  }

  async openNodes(names: string[]): Promise<KnowledgeGraph> {
    const graph = await this.loadGraph()

    // Filter entities
    const filteredEntities = graph.entities.filter((e) => names.includes(e.name))

    // Create a Set of filtered entity names for quick lookup
    const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))

    // Filter relations to only include those between filtered entities
    const filteredRelations = graph.relations.filter(
      (r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
    )

    const filteredGraph: KnowledgeGraph = {
      entities: filteredEntities,
      relations: filteredRelations
    }

    return filteredGraph
  }
}

class MemoryServer {
  public server: Server
  private knowledgeGraphManager: KnowledgeGraphManager

  constructor(envPath: string = '') {
    const memoryPath = envPath
      ? path.isAbsolute(envPath)
        ? envPath
        : path.join(path.dirname(fileURLToPath(import.meta.url)), envPath)
      : defaultMemoryPath
    this.knowledgeGraphManager = new KnowledgeGraphManager(memoryPath)
    this.server = new Server(
      {
        name: 'memory-server',
        version: '1.0.0'
      },
      {
        capabilities: {
          tools: {}
        }
      }
    )
    this.initialize()
  }

  initialize() {
    // The server instance and tools exposed to Claude
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: 'create_entities',
            description: 'Create multiple new entities in the knowledge graph',
            inputSchema: {
              type: 'object',
              properties: {
                entities: {
                  type: 'array',
                  items: {
                    type: 'object',
                    properties: {
                      name: { type: 'string', description: 'The name of the entity' },
                      entityType: { type: 'string', description: 'The type of the entity' },
                      observations: {
                        type: 'array',
                        items: { type: 'string' },
                        description: 'An array of observation contents associated with the entity'
                      }
                    },
                    required: ['name', 'entityType', 'observations']
                  }
                }
              },
              required: ['entities']
            }
          },
          {
            name: 'create_relations',
            description:
              'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
            inputSchema: {
              type: 'object',
              properties: {
                relations: {
                  type: 'array',
                  items: {
                    type: 'object',
                    properties: {
                      from: { type: 'string', description: 'The name of the entity where the relation starts' },
                      to: { type: 'string', description: 'The name of the entity where the relation ends' },
                      relationType: { type: 'string', description: 'The type of the relation' }
                    },
                    required: ['from', 'to', 'relationType']
                  }
                }
              },
              required: ['relations']
            }
          },
          {
            name: 'add_observations',
            description: 'Add new observations to existing entities in the knowledge graph',
            inputSchema: {
              type: 'object',
              properties: {
                observations: {
                  type: 'array',
                  items: {
                    type: 'object',
                    properties: {
                      entityName: { type: 'string', description: 'The name of the entity to add the observations to' },
                      contents: {
                        type: 'array',
                        items: { type: 'string' },
                        description: 'An array of observation contents to add'
                      }
                    },
                    required: ['entityName', 'contents']
                  }
                }
              },
              required: ['observations']
            }
          },
          {
            name: 'delete_entities',
            description: 'Delete multiple entities and their associated relations from the knowledge graph',
            inputSchema: {
              type: 'object',
              properties: {
                entityNames: {
                  type: 'array',
                  items: { type: 'string' },
                  description: 'An array of entity names to delete'
                }
              },
              required: ['entityNames']
            }
          },
          {
            name: 'delete_observations',
            description: 'Delete specific observations from entities in the knowledge graph',
            inputSchema: {
              type: 'object',
              properties: {
                deletions: {
                  type: 'array',
                  items: {
                    type: 'object',
                    properties: {
                      entityName: { type: 'string', description: 'The name of the entity containing the observations' },
                      observations: {
                        type: 'array',
                        items: { type: 'string' },
                        description: 'An array of observations to delete'
                      }
                    },
                    required: ['entityName', 'observations']
                  }
                }
              },
              required: ['deletions']
            }
          },
          {
            name: 'delete_relations',
            description: 'Delete multiple relations from the knowledge graph',
            inputSchema: {
              type: 'object',
              properties: {
                relations: {
                  type: 'array',
                  items: {
                    type: 'object',
                    properties: {
                      from: { type: 'string', description: 'The name of the entity where the relation starts' },
                      to: { type: 'string', description: 'The name of the entity where the relation ends' },
                      relationType: { type: 'string', description: 'The type of the relation' }
                    },
                    required: ['from', 'to', 'relationType']
                  },
                  description: 'An array of relations to delete'
                }
              },
              required: ['relations']
            }
          },
          {
            name: 'read_graph',
            description: 'Read the entire knowledge graph',
            inputSchema: {
              type: 'object',
              properties: {}
            }
          },
          {
            name: 'search_nodes',
            description: 'Search for nodes in the knowledge graph based on a query',
            inputSchema: {
              type: 'object',
              properties: {
                query: {
                  type: 'string',
                  description: 'The search query to match against entity names, types, and observation content'
                }
              },
              required: ['query']
            }
          },
          {
            name: 'open_nodes',
            description: 'Open specific nodes in the knowledge graph by their names',
            inputSchema: {
              type: 'object',
              properties: {
                names: {
                  type: 'array',
                  items: { type: 'string' },
                  description: 'An array of entity names to retrieve'
                }
              },
              required: ['names']
            }
          }
        ]
      }
    })

    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params

      if (!args) {
        throw new Error(`No arguments provided for tool: ${name}`)
      }

      switch (name) {
        case 'create_entities':
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(
                  await this.knowledgeGraphManager.createEntities(args.entities as Entity[]),
                  null,
                  2
                )
              }
            ]
          }
        case 'create_relations':
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(
                  await this.knowledgeGraphManager.createRelations(args.relations as Relation[]),
                  null,
                  2
                )
              }
            ]
          }
        case 'add_observations':
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(
                  await this.knowledgeGraphManager.addObservations(
                    args.observations as { entityName: string; contents: string[] }[]
                  ),
                  null,
                  2
                )
              }
            ]
          }
        case 'delete_entities':
          await this.knowledgeGraphManager.deleteEntities(args.entityNames as string[])
          return { content: [{ type: 'text', text: 'Entities deleted successfully' }] }
        case 'delete_observations':
          await this.knowledgeGraphManager.deleteObservations(
            args.deletions as { entityName: string; observations: string[] }[]
          )
          return { content: [{ type: 'text', text: 'Observations deleted successfully' }] }
        case 'delete_relations':
          await this.knowledgeGraphManager.deleteRelations(args.relations as Relation[])
          return { content: [{ type: 'text', text: 'Relations deleted successfully' }] }
        case 'read_graph':
          return {
            content: [{ type: 'text', text: JSON.stringify(await this.knowledgeGraphManager.readGraph(), null, 2) }]
          }
        case 'search_nodes':
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(await this.knowledgeGraphManager.searchNodes(args.query as string), null, 2)
              }
            ]
          }
        case 'open_nodes':
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(await this.knowledgeGraphManager.openNodes(args.names as string[]), null, 2)
              }
            ]
          }
        default:
          throw new Error(`Unknown tool: ${name}`)
      }
    })
  }
}

export default MemoryServer
